ปลดล็อกพลังของ State Machine ใน React ด้วย Custom Hook เรียนรู้การจัดการ Logic ที่ซับซ้อน เพิ่มความสามารถในการบำรุงรักษาโค้ด และสร้างแอปพลิเคชันที่แข็งแกร่ง
React Custom Hook State Machine: การจัดการ State Logic ที่ซับซ้อนอย่างมืออาชีพ
เมื่อแอปพลิเคชัน React มีความซับซ้อนมากขึ้น การจัดการ state อาจกลายเป็นความท้าทายที่สำคัญ แนวทางแบบดั้งเดิมที่ใช้ `useState` และ `useEffect` อาจทำให้เกิด logic ที่พันกันและโค้ดที่ดูแลรักษายากได้อย่างรวดเร็ว โดยเฉพาะเมื่อต้องจัดการกับการเปลี่ยน state และ side effect ที่ซับซ้อน นี่คือจุดที่ state machine และโดยเฉพาะอย่างยิ่ง React custom hook ที่นำมาประยุกต์ใช้ จะเข้ามาช่วยแก้ไขปัญหานี้ บทความนี้จะแนะนำคุณเกี่ยวกับแนวคิดของ state machine สาธิตวิธีการสร้างเป็น custom hook ใน React และแสดงให้เห็นถึงประโยชน์ที่ได้รับจากการสร้างแอปพลิเคชันที่สามารถขยายขนาดและบำรุงรักษาได้สำหรับผู้ใช้ทั่วโลก
State Machine คืออะไร?
State machine (หรือ finite state machine, FSM) คือโมเดลทางคณิตศาสตร์ของการคำนวณที่อธิบายพฤติกรรมของระบบโดยการกำหนดจำนวน state ที่มีจำกัดและการเปลี่ยนผ่านระหว่าง state เหล่านั้น ลองนึกภาพเหมือนแผนผัง (flowchart) แต่มีกฎที่เข้มงวดกว่าและคำจำกัดความที่เป็นทางการมากกว่า แนวคิดหลักประกอบด้วย:
- States (สถานะ): แทนเงื่อนไขหรือช่วงระยะต่างๆ ของระบบ
- Transitions (การเปลี่ยนผ่าน): กำหนดว่าระบบจะเคลื่อนที่จาก state หนึ่งไปยังอีก state หนึ่งได้อย่างไร โดยขึ้นอยู่กับเหตุการณ์หรือเงื่อนไขเฉพาะ
- Events (เหตุการณ์): สิ่งกระตุ้นที่ทำให้เกิดการเปลี่ยน state
- Initial State (สถานะเริ่มต้น): สถานะที่ระบบเริ่มต้นทำงาน
State machine เหมาะอย่างยิ่งสำหรับการสร้างโมเดลระบบที่มี state ที่กำหนดไว้อย่างดีและการเปลี่ยนผ่านที่ชัดเจน มีตัวอย่างมากมายในสถานการณ์จริง:
- สัญญาณไฟจราจร: วนไปตาม state ต่างๆ เช่น สีแดง สีเหลือง สีเขียว โดยมีการเปลี่ยนผ่านที่ถูกกระตุ้นโดยตัวจับเวลา นี่เป็นตัวอย่างที่รู้จักกันทั่วโลก
- การประมวลผลคำสั่งซื้อ: คำสั่งซื้อใน e-commerce อาจเปลี่ยนผ่าน state ต่างๆ เช่น "รอดำเนินการ" (Pending) "กำลังดำเนินการ" (Processing) "จัดส่งแล้ว" (Shipped) และ "ส่งมอบแล้ว" (Delivered) ซึ่งใช้ได้กับธุรกิจค้าปลีกออนไลน์ทั่วโลก
- กระบวนการยืนยันตัวตน: กระบวนการยืนยันตัวตนผู้ใช้อาจมี state เช่น "ออกจากระบบแล้ว" (Logged Out) "กำลังเข้าสู่ระบบ" (Logging In) "เข้าสู่ระบบแล้ว" (Logged In) และ "เกิดข้อผิดพลาด" (Error) โปรโตคอลความปลอดภัยโดยทั่วไปมีความสอดคล้องกันในทุกประเทศ
ทำไมต้องใช้ State Machine ใน React?
การนำ State machine มาใช้ในคอมโพเนนต์ React ของคุณมีข้อดีที่น่าสนใจหลายประการ:
- การจัดระเบียบโค้ดที่ดีขึ้น: State machine บังคับให้มีแนวทางที่เป็นโครงสร้างในการจัดการ state ทำให้โค้ดของคุณคาดเดาได้ง่ายและเข้าใจง่ายขึ้น ไม่มี spaghetti code อีกต่อไป!
- ลดความซับซ้อน: ด้วยการกำหนด state และ transition อย่างชัดเจน คุณสามารถลดความซับซ้อนของ logic ที่ซับซ้อนและหลีกเลี่ยง side effect ที่ไม่คาดคิดได้
- เพิ่มความสามารถในการทดสอบ: โดยธรรมชาติแล้ว State machine สามารถทดสอบได้ง่าย คุณสามารถตรวจสอบได้อย่างง่ายดายว่าระบบของคุณทำงานถูกต้องหรือไม่โดยการทดสอบแต่ละ state และ transition
- เพิ่มความสามารถในการบำรุงรักษา: ลักษณะการเขียนโปรแกรมแบบ declarative ของ state machine ทำให้ง่ายต่อการแก้ไขและขยายโค้ดของคุณเมื่อแอปพลิเคชันของคุณพัฒนาขึ้น
- การแสดงภาพที่ดีขึ้น: มีเครื่องมือที่สามารถแสดงภาพ state machine ซึ่งให้ภาพรวมที่ชัดเจนของพฤติกรรมของระบบของคุณ ช่วยในการทำงานร่วมกันและความเข้าใจระหว่างทีมที่มีทักษะหลากหลาย
การสร้าง State Machine ในรูปแบบ React Custom Hook
เรามาดูตัวอย่างวิธีการสร้าง State machine โดยใช้ React custom hook กัน เราจะสร้างตัวอย่างง่ายๆ ของปุ่มที่สามารถอยู่ในสาม state: `idle`, `loading` และ `success` ปุ่มจะเริ่มต้นใน state `idle` เมื่อคลิก จะเปลี่ยนไปที่ state `loading` จำลองกระบวนการโหลด (โดยใช้ `setTimeout`) แล้วจึงเปลี่ยนไปที่ state `success`
1. กำหนด State Machine
ขั้นแรก เราจะกำหนด state และ transition ของ state machine สำหรับปุ่มของเรา:
const buttonStateMachineDefinition = {
initial: 'idle',
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success', // After 2 seconds, transition to success
},
},
success: {},
},
};
การกำหนดค่านี้ใช้วิธีที่ไม่ขึ้นกับไลบรารีใดไลบรารีหนึ่ง (แม้ว่าจะได้รับแรงบันดาลใจจาก XState) ในการกำหนด state machine เราจะสร้าง logic เพื่อตีความการกำหนดค่านี้ด้วยตัวเองใน custom hook property `initial` กำหนด state เริ่มต้นเป็น `idle` property `states` กำหนด state ที่เป็นไปได้ (`idle`, `loading` และ `success`) และ transition ของมัน state `idle` มี property `on` ที่กำหนด transition ไปยัง state `loading` เมื่อเกิด event `CLICK` state `loading` ใช้ property `after` เพื่อเปลี่ยนไปยัง state `success` โดยอัตโนมัติหลังจาก 2000 มิลลิวินาที (2 วินาที) state `success` เป็น state สุดท้ายในตัวอย่างนี้
2. สร้าง Custom Hook
ตอนนี้ เรามาสร้าง custom hook ที่จะจัดการตรรกะของ state machine กัน:
import { useState, useEffect } from 'react';
const useStateMachine = (stateMachineDefinition) => {
const [currentState, setCurrentState] = useState(stateMachineDefinition.initial);
const [context, setContext] = useState({});
const transition = (event) => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.on && stateDefinition.on[event]) {
setCurrentState(stateDefinition.on[event]);
}
};
useEffect(() => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.after) {
const timeoutKeys = Object.keys(stateDefinition.after);
timeoutKeys.forEach(timeoutKey => {
const timeout = parseInt(timeoutKey, 10);
const nextState = stateDefinition.after[timeoutKey];
const timer = setTimeout(() => {
setCurrentState(nextState);
clearTimeout(timer);
}, timeout);
return () => clearTimeout(timer); // Cleanup on unmount or state change
});
}
}, [currentState, stateMachineDefinition.states]);
return {
currentState,
context,
transition,
};
};
export default useStateMachine;
hook `useStateMachine` นี้รับค่าการกำหนด state machine เป็นอาร์กิวเมนต์ มันใช้ `useState` เพื่อจัดการ state ปัจจุบันและ context (เราจะอธิบาย context ในภายหลัง) ฟังก์ชัน `transition` รับ event เป็นอาร์กิวเมนต์และอัปเดต state ปัจจุบันตาม transition ที่กำหนดไว้ในการกำหนด state machine hook `useEffect` จัดการ property `after` โดยตั้งเวลาเพื่อเปลี่ยนไปยัง state ถัดไปโดยอัตโนมัติหลังจากระยะเวลาที่กำหนด hook จะคืนค่า state ปัจจุบัน, context และฟังก์ชัน `transition`
3. ใช้ Custom Hook ใน Component
สุดท้าย เราจะนำ custom hook ไปใช้ในคอมโพเนนต์ React:
import React from 'react';
import useStateMachine from './useStateMachine';
const buttonStateMachineDefinition = {
initial: 'idle',
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success', // After 2 seconds, transition to success
},
},
success: {},
},
};
const MyButton = () => {
const { currentState, transition } = useStateMachine(buttonStateMachineDefinition);
const handleClick = () => {
if (currentState === 'idle') {
transition('CLICK');
}
};
let buttonText = 'Click Me';
if (currentState === 'loading') {
buttonText = 'Loading...';
} else if (currentState === 'success') {
buttonText = 'Success!';
}
return (
);
};
export default MyButton;
คอมโพเนนต์นี้ใช้ hook `useStateMachine` เพื่อจัดการ state ของปุ่ม ฟังก์ชัน `handleClick` จะส่ง event `CLICK` เมื่อปุ่มถูกคลิก (และเฉพาะเมื่ออยู่ใน state `idle` เท่านั้น) คอมโพเนนต์จะแสดงข้อความที่แตกต่างกันตาม state ปัจจุบัน ปุ่มจะถูกปิดใช้งานขณะกำลังโหลดเพื่อป้องกันการคลิกซ้ำซ้อน
การจัดการ Context ใน State Machine
ในสถานการณ์จริงหลายกรณี state machine จำเป็นต้องจัดการข้อมูลที่คงอยู่ตลอดการเปลี่ยน state ข้อมูลนี้เรียกว่า context Context ช่วยให้คุณสามารถจัดเก็บและอัปเดตข้อมูลที่เกี่ยวข้องในขณะที่ state machine ดำเนินไป
เรามาขยายตัวอย่างปุ่มของเราให้มีตัวนับที่เพิ่มขึ้นทุกครั้งที่ปุ่มโหลดสำเร็จ เราจะแก้ไขการกำหนด state machine และ custom hook เพื่อจัดการ context
1. อัปเดตการกำหนด State Machine
const buttonStateMachineDefinition = {
initial: 'idle',
context: {
count: 0,
},
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success',
},
},
success: {
entry: (context) => {
return { ...context, count: context.count + 1 };
},
},
},
};
เราได้เพิ่ม property `context` เข้าไปในการกำหนด state machine โดยมีค่าเริ่มต้น `count` เป็น 0 นอกจากนี้เรายังได้เพิ่ม `entry` action ให้กับ state `success` ด้วย `entry` action จะถูกเรียกใช้เมื่อ state machine เข้าสู่ state `success` มันจะรับ context ปัจจุบันเป็นอาร์กิวเมนต์และคืนค่า context ใหม่ที่ `count` ถูกเพิ่มขึ้น `entry` ที่นี่แสดงตัวอย่างของการแก้ไข context เนื่องจากอ็อบเจกต์ Javascript ถูกส่งผ่านแบบ reference จึงเป็นเรื่องสำคัญที่จะต้องคืนค่าอ็อบเจกต์ *ใหม่* แทนที่จะแก้ไขอ็อบเจกต์เดิม
2. อัปเดต Custom Hook
import { useState, useEffect } from 'react';
const useStateMachine = (stateMachineDefinition) => {
const [currentState, setCurrentState] = useState(stateMachineDefinition.initial);
const [context, setContext] = useState(stateMachineDefinition.context || {});
const transition = (event) => {
const stateDefinition = stateMachineDefinition.states[currentState];
if (stateDefinition && stateDefinition.on && stateDefinition.on[event]) {
setCurrentState(stateDefinition.on[event]);
}
};
useEffect(() => {
const stateDefinition = stateMachineDefinition.states[currentState];
if(stateDefinition && stateDefinition.entry){
const newContext = stateDefinition.entry(context);
setContext(newContext);
}
if (stateDefinition && stateDefinition.after) {
const timeoutKeys = Object.keys(stateDefinition.after);
timeoutKeys.forEach(timeoutKey => {
const timeout = parseInt(timeoutKey, 10);
const nextState = stateDefinition.after[timeoutKey];
const timer = setTimeout(() => {
setCurrentState(nextState);
clearTimeout(timer);
}, timeout);
return () => clearTimeout(timer); // Cleanup on unmount or state change
});
}
}, [currentState, stateMachineDefinition.states, context]);
return {
currentState,
context,
transition,
};
};
export default useStateMachine;
เราได้อัปเดต hook `useStateMachine` เพื่อเริ่มต้น state ของ `context` ด้วย `stateMachineDefinition.context` หรืออ็อบเจกต์ว่างหากไม่มี context ให้มา นอกจากนี้เรายังได้เพิ่ม `useEffect` เพื่อจัดการ `entry` action เมื่อ state ปัจจุบันมี `entry` action เราจะเรียกใช้มันและอัปเดต context ด้วยค่าที่ส่งคืนกลับมา
3. ใช้ Hook ที่อัปเดตแล้วใน Component
import React from 'react';
import useStateMachine from './useStateMachine';
const buttonStateMachineDefinition = {
initial: 'idle',
context: {
count: 0,
},
states: {
idle: {
on: {
CLICK: 'loading',
},
},
loading: {
after: {
2000: 'success',
},
},
success: {
entry: (context) => {
return { ...context, count: context.count + 1 };
},
},
},
};
const MyButton = () => {
const { currentState, context, transition } = useStateMachine(buttonStateMachineDefinition);
const handleClick = () => {
if (currentState === 'idle') {
transition('CLICK');
}
};
let buttonText = 'Click Me';
if (currentState === 'loading') {
buttonText = 'Loading...';
} else if (currentState === 'success') {
buttonText = 'Success!';
}
return (
Count: {context.count}
);
};
export default MyButton;
ตอนนี้เราสามารถเข้าถึง `context.count` ในคอมโพเนนต์และนำมาแสดงผลได้ ทุกครั้งที่ปุ่มโหลดสำเร็จ ตัวนับจะเพิ่มขึ้น
แนวคิด State Machine ขั้นสูง
แม้ว่าตัวอย่างของเราจะค่อนข้างง่าย แต่ state machine สามารถจัดการกับสถานการณ์ที่ซับซ้อนกว่านี้ได้มาก นี่คือแนวคิดขั้นสูงบางส่วนที่ควรพิจารณา:
- Guards (เงื่อนไขป้องกัน): เงื่อนไขที่ต้องเป็นจริงเพื่อให้การเปลี่ยนผ่านเกิดขึ้นได้ ตัวอย่างเช่น การเปลี่ยนผ่านอาจได้รับอนุญาตเฉพาะเมื่อผู้ใช้ได้รับการยืนยันตัวตนแล้ว หรือเมื่อค่าข้อมูลบางอย่างเกินเกณฑ์ที่กำหนด
- Actions (การกระทำ): Side effect ที่จะถูกเรียกใช้เมื่อเข้าสู่หรือออกจาก state ซึ่งอาจรวมถึงการเรียก API, การอัปเดต DOM หรือการส่ง event ไปยังคอมโพเนนต์อื่น
- Parallel States (สถานะคู่ขนาน): ช่วยให้คุณสามารถสร้างโมเดลระบบที่มีกิจกรรมหลายอย่างเกิดขึ้นพร้อมกันได้ ตัวอย่างเช่น เครื่องเล่นวิดีโออาจมี state machine หนึ่งสำหรับควบคุมการเล่น (play, pause, stop) และอีกหนึ่งสำหรับจัดการคุณภาพวิดีโอ (low, medium, high)
- Hierarchical States (สถานะแบบลำดับชั้น): ช่วยให้คุณสามารถซ้อน state ภายใน state อื่นๆ ได้ ทำให้เกิดลำดับชั้นของ state ซึ่งมีประโยชน์สำหรับการสร้างโมเดลระบบที่ซับซ้อนซึ่งมี state ที่เกี่ยวข้องกันจำนวนมาก
ไลบรารีทางเลือก: XState และอื่นๆ
แม้ว่า custom hook ของเราจะให้การใช้งานพื้นฐานของ state machine แต่ก็มีไลบรารีที่ยอดเยี่ยมหลายตัวที่สามารถทำให้กระบวนการง่ายขึ้นและมีฟีเจอร์ขั้นสูงกว่า
XState
XState เป็นไลบรารี JavaScript ที่ได้รับความนิยมสำหรับการสร้าง ตีความ และเรียกใช้งาน state machine และ statechart มันมี API ที่ทรงพลังและยืดหยุ่นสำหรับการกำหนด state machine ที่ซับซ้อน รวมถึงการรองรับ guards, actions, parallel states และ hierarchical states XState ยังมีเครื่องมือที่ยอดเยี่ยมสำหรับการแสดงภาพและดีบัก state machine
ไลบรารีอื่นๆ
ตัวเลือกอื่นๆ ได้แก่:
- Robot: ไลบรารีการจัดการ state ขนาดเล็กที่เน้นความเรียบง่ายและประสิทธิภาพ
- react-automata: ไลบรารีที่ออกแบบมาโดยเฉพาะสำหรับการนำ state machine มาใช้ในคอมโพเนนต์ React
การเลือกไลบรารีขึ้นอยู่กับความต้องการเฉพาะของโปรเจกต์ของคุณ XState เป็นตัวเลือกที่ดีสำหรับ state machine ที่ซับซ้อน ในขณะที่ Robot และ react-automata เหมาะสำหรับสถานการณ์ที่เรียบง่ายกว่า
แนวทางปฏิบัติที่ดีที่สุดสำหรับการใช้ State Machine
เพื่อใช้ประโยชน์จาก state machine ในแอปพลิเคชัน React ของคุณอย่างมีประสิทธิภาพ ลองพิจารณาแนวทางปฏิบัติที่ดีที่สุดต่อไปนี้:
- เริ่มต้นจากสิ่งเล็กๆ: เริ่มต้นด้วย state machine ที่เรียบง่ายและค่อยๆ เพิ่มความซับซ้อนตามความจำเป็น
- แสดงภาพ State Machine ของคุณ: ใช้เครื่องมือแสดงภาพเพื่อทำความเข้าใจพฤติกรรมของ state machine ของคุณอย่างชัดเจน
- เขียนการทดสอบที่ครอบคลุม: ทดสอบแต่ละ state และ transition อย่างละเอียดเพื่อให้แน่ใจว่าระบบของคุณทำงานถูกต้อง
- จัดทำเอกสาร State Machine ของคุณ: จัดทำเอกสารเกี่ยวกับ state, transition, guard และ action ของ state machine ของคุณอย่างชัดเจน
- พิจารณาเรื่อง Internationalization (i18n): หากแอปพลิเคชันของคุณมีเป้าหมายเป็นผู้ใช้ทั่วโลก ตรวจสอบให้แน่ใจว่า logic ของ state machine และส่วนติดต่อผู้ใช้ของคุณได้รับการปรับให้เข้ากับภาษาและวัฒนธรรมต่างๆ อย่างเหมาะสม ตัวอย่างเช่น ใช้ state machine หรือ context แยกกันเพื่อจัดการรูปแบบวันที่หรือสัญลักษณ์สกุลเงินที่แตกต่างกันตามท้องถิ่นของผู้ใช้
- การเข้าถึง (Accessibility - a11y): ตรวจสอบให้แน่ใจว่าการเปลี่ยน state และการอัปเดต UI ของคุณสามารถเข้าถึงได้โดยผู้ใช้ที่มีความพิการ ใช้ ARIA attributes และ HTML เชิงความหมายเพื่อให้บริบทและข้อเสนอแนะที่เหมาะสมกับเทคโนโลยีสิ่งอำนวยความสะดวก
สรุป
การใช้ React custom hook ร่วมกับ state machine เป็นแนวทางที่ทรงพลังและมีประสิทธิภาพในการจัดการ state logic ที่ซับซ้อนในแอปพลิเคชัน React ด้วยการแยกการเปลี่ยน state และ side effect ออกมาเป็นโมเดลที่กำหนดไว้อย่างดี คุณสามารถปรับปรุงการจัดระเบียบโค้ด ลดความซับซ้อน เพิ่มความสามารถในการทดสอบ และเพิ่มความสามารถในการบำรุงรักษาได้ ไม่ว่าคุณจะสร้าง custom hook ของคุณเองหรือใช้ไลบรารีอย่าง XState การนำ state machine เข้ามาในกระบวนการทำงาน React ของคุณสามารถปรับปรุงคุณภาพและความสามารถในการขยายขนาดของแอปพลิเคชันสำหรับผู้ใช้ทั่วโลกได้อย่างมีนัยสำคัญ